package com.doge.walknovel.payment.google

import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Handler
import android.os.Looper
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.OnLifecycleEvent
import com.android.billingclient.api.*
import com.doge.wnpro.R
import com.doge.walknovel.analysis.analysisEvent
import com.doge.walknovel.utils.*
import java.util.*

class AppBillingClientLifecycle(private val appContext: Context) : LifecycleObserver,
    PurchasesUpdatedListener, BillingClientStateListener, SkuDetailsResponseListener {
    private var billingClient: BillingClient? = null
    private var lifecycleRef: Lifecycle? = null
    private var silently = false
    private var refer: String? = null

    private var hasSubscriptionBusiness = false

    /**
     * SkuDetails for all known SKUs.
     */
    val inappSkuDetails = MutableLiveData<Map<String, SkuDetails>>()
    val subsSkuDetails = MutableLiveData<Map<String, SkuDetails>>()

    /**
     * Purchases are observable. This list will be updated when the Billing Library
     * detects new or existing purchases. All observers will be notified.
     */
    val purchaseUpdateEvent = MutableLiveData<List<Purchase>>()
    val purchaseSyncedMissedEvent = MutableLiveData<Any?>()

    companion object {
        const val TAG = "googlePay"

        fun get() = getInstance(appContext as Application)

        @Volatile
        private var INSTANCE: AppBillingClientLifecycle? = null
        private fun getInstance(app: Application): AppBillingClientLifecycle {
            if (INSTANCE == null) {
                synchronized(AppBillingClientLifecycle::class.java) {
                    if (INSTANCE == null) {
                        INSTANCE = AppBillingClientLifecycle(app)
                    }
                }
            }
            return INSTANCE!!
        }
    }


    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun create() {
        log("$TAG lifecycle onCreate")
        // Create a new BillingClient in onCreate().
        // Since the BillingClient can only be used once, we need to create a new instance
        // after ending the previous connection to the Google Play Store in onDestroy().
        if (billingClient != null) return // already launched.

        val client = provideBillingClient().also { billingClient = it }
        if (!client.isReady) {
            log("$TAG BillingClient: Start connection...")
            client.startConnection(this)
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun destroy() {
        this.lifecycleRef = null
        log("$TAG lifecycle onDestroy $billingClient")
        val client = billingClient ?: return // already destroyed.
        if (client.isReady) {
            log("$TAG BillingClient: closing connection (can only be used once)")
            // BillingClient can only be used once.
            // After calling endConnection(), we must create a new BillingClient.
            client.endConnection()
            billingClient = null
        }
    }

    override fun onBillingServiceDisconnected() {
        log("$TAG onBillingServiceDisconnected")
        // Try connecting again with exponential backoff.
    }

    override fun onBillingSetupFinished(billingResult: BillingResult) {
        val responseCode: Int = billingResult.responseCode
        val debugMessage: String = billingResult.debugMessage
        feedback("1.setup", "$responseCode ${respCodeDesc(responseCode)} $debugMessage")
        when (responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                // The billing client is ready. You can query purchases here.
                AppPayHelper.skuList { inapp, subs ->
                    querySkuDetails(BillingClient.SkuType.INAPP, inapp)
                    if (hasSubscriptionBusiness) {
                        querySkuDetails(BillingClient.SkuType.SUBS, subs)
                    }
                }
                queryPurchases(BillingClient.SkuType.INAPP)
                if (hasSubscriptionBusiness) {
                    queryPurchases(BillingClient.SkuType.SUBS)
                }
            }
            BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> {
                billingToast(debugMessage) // Billing service unavailable on device.
            }
        }
    }

    /**
     * In order to make purchases, you need the [SkuDetails] for the item or subscription.
     * This is an asynchronous call that will receive a result in [.onSkuDetailsResponse].
     */
    private fun querySkuDetails(type: String, skus: List<String>) {
        if (skus.isEmpty()) return // service product data list is null

        log("$TAG querySkuDetailsAsync type $type skus $skus $billingClient")
        val params = SkuDetailsParams.newBuilder()
            .setType(type)
            .setSkusList(skus)
            .build()
        billingClient?.querySkuDetailsAsync(params, this)
    }

    /**
     * Receives the result from {@link #querySkuDetails()}}.
     * <p>
     * Store the SkuDetails and post them in the {@link #skusWithSkuDetails}. This allows other
     * parts of the app to use the {@link SkuDetails} to show SKU information and make purchases.
     */
    override fun onSkuDetailsResponse(
        billingResult: BillingResult,
        skuDetailsList: MutableList<SkuDetails>?
    ) {
        val responseCode: Int = billingResult.responseCode
        val debugMessage: String = billingResult.debugMessage

        val sdkType = skuDetailsList?.firstOrNull()?.type ?: ""
        feedback(
            "2.sdkDetails loaded",
            "$sdkType $responseCode ${respCodeDesc(responseCode)} $debugMessage"
        )

        when (responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                log(
                    "onSkuDetailsResponse $responseCode $debugMessage size ${skuDetailsList?.size ?: 0} \n ${
                        skuDetailsList?.joinToString(
                            "\n"
                        )
                    }"
                )
                val respList = skuDetailsList ?: listOf()
                val newSkusDetailList: MutableMap<String, SkuDetails> = HashMap()
                respList.forEach { newSkusDetailList[it.sku] = it }
                when (respList.firstOrNull()?.type) {
                    BillingClient.SkuType.INAPP -> inappSkuDetails.postValue(newSkusDetailList)
                    BillingClient.SkuType.SUBS -> subsSkuDetails.postValue(newSkusDetailList)
                }
            }
            BillingClient.BillingResponseCode.ERROR -> {
                billingToast("Billing service $debugMessage") // An internal error occurred.
            }
        }
    }

    /**
     * Query Google Play Billing for existing purchases.
     *
     *
     * New purchases will be provided to the PurchasesUpdatedListener.
     * You still need to check the Google Play Billing API to know when purchase tokens are removed.
     */
    private fun queryPurchases(type: String) {
        if (billingClient?.isReady != true) {
            log("$TAG queryPurchases BillingClient is not ready")
        }
        log("$TAG queryPurchases type $type $billingClient")
        billingClient?.queryPurchasesAsync(type) { _, purchasesList ->
            log("$TAG queryPurchases purchase size ${purchasesList.size}")
            processPurchases(purchasesList)
        }
    }

    /**
     * Called by the Billing Library when new purchases are detected.
     */
    override fun onPurchasesUpdated(
        billingResult: BillingResult,
        purchases: MutableList<Purchase>?
    ) {
        val responseCode: Int = billingResult.responseCode
        val debugMessage: String = billingResult.debugMessage
        feedback(
            "4.purchases updated",
            "$responseCode ${respCodeDesc(responseCode)} $debugMessage [${purchases?.size}]"
        )

        when (responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                log(
                    "onPurchasesUpdated $responseCode $debugMessage \n purchases[${purchases?.size ?: 0} \n ${
                        purchases?.joinToString(
                            "-"
                        )
                    }"
                )
                processPurchases(purchases)       // [count:410]
            }
            BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
                // The last order is being settled, wait a few seconds  // Item is already owned.
            }
            BillingClient.BillingResponseCode.USER_CANCELED -> {
                // user canceled                     [count:72]
            }
            BillingClient.BillingResponseCode.ERROR -> {
                // Server error, please try again.   [count:2]
                // empty reason                      [count:12]
            }
        }
    }

    /**
     * Send purchase SingleLiveEvent and update purchases LiveData.
     *
     * The SingleLiveEvent will trigger network call to verify the subscriptions on the sever.
     * The LiveData will allow Google Play settings UI to update based on the latest purchase data.
     *
     * https://developer.android.google.cn/reference/com/android/billingclient/api/BillingClient?hl=zh-cn#consumeAsync
     */
    private fun processPurchases(purchasesList: List<Purchase>?) {
        if (purchasesList?.isNotEmpty() == true) {
            log("$TAG processPurchases ${purchasesList.size} purchase(s)")
        } else {
            log("$TAG processPurchases with no purchases")
            sentPurchaseSyncedMissedEvent()
        }

        purchasesList?.forEach { purchase ->
            AppPayHelper.syncPurchase(purchase, refer) {
                if (!it) {
                    sentPurchaseSyncedMissedEvent()
                } else {
                    when (AppPayHelper.skuTypeByProductId(purchase.sku)) {
                        BillingClient.SkuType.INAPP -> {
                            log("$TAG consumeAsyncPurchase inapp ${purchase.purchaseToken}")
                            val params = ConsumeParams.newBuilder()
                                .setPurchaseToken(purchase.purchaseToken)
                                .build()
                            billingClient?.consumeAsync(params) { billingResult, _ ->
                                // maybe lost callback
                                val responseCode = billingResult.responseCode
                                val debugMessage = billingResult.debugMessage
                                feedback(
                                    "6.consume inapp purchase",
                                    "${purchase.sku} $responseCode ${respCodeDesc(responseCode)} $debugMessage"
                                )

                                if (responseCode != BillingClient.BillingResponseCode.OK) {
                                    // SERVICE_DISCONNECTED Service connection is disconnected.
                                    // SERVICE_UNAVAILABLE An internal error occurred.
                                    billingToast("Billing service $debugMessage")
                                }

                                sentPurchaseUpdateEvent(billingResult, purchasesList)
                                sentPurchaseSyncedMissedEvent()
                            }
                        }
                        BillingClient.SkuType.SUBS -> {
                            log("$TAG acknowledgePurchase subs ${purchase.isAcknowledged} ${purchase.purchaseToken}")
                            val params = AcknowledgePurchaseParams.newBuilder()
                                .setPurchaseToken(purchase.purchaseToken)
                                .build()
                            billingClient?.acknowledgePurchase(params) { billingResult ->
                                // maybe lost callback
                                val responseCode = billingResult.responseCode
                                val debugMessage = billingResult.debugMessage
                                log("$TAG acknowledgePurchase subs result $responseCode $debugMessage")
                                if (purchase.isAcknowledged) return@acknowledgePurchase // 不用给log?
                                feedback(
                                    "6.acknowledge subs purchase",
                                    "${purchase.sku} $responseCode ${respCodeDesc(responseCode)} $debugMessage"
                                )

                                if (responseCode != BillingClient.BillingResponseCode.OK) {
                                    // SERVICE_DISCONNECTED Service connection is disconnected.
                                    // SERVICE_UNAVAILABLE An internal error occurred.
                                    billingToast("Billing service $debugMessage")
                                }

                                sentPurchaseUpdateEvent(billingResult, purchasesList)
                                sentPurchaseSyncedMissedEvent()
                            }
                        }
                    }
                }
            }
        }
    }

    fun launchBillingFlow(
        activity: Activity,
        @BillingClient.SkuType skuType: String,
        productId: String
    ) {
        val billingResult = internalLaunchBillingFlow(activity, skuType, productId)
        val responseCode = billingResult.responseCode
        val debugMessage = billingResult.debugMessage
        feedback(
            "3.launchBillingFlow",
            "$productId $responseCode ${respCodeDesc(responseCode)} $debugMessage"
        )
        when (responseCode) {
            BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
                // skip analysis // The last order is being settled, wait a few seconds
                billingToast(getString(R.string.the_last_order_is_being_settled_wait_a_few_seconds))  // Item is already owned.
            }
            BillingClient.BillingResponseCode.ERROR -> {
                // BillingClient is not ready
                // SkuDetails is null
                billingToast("Billing service $debugMessage")
            }
        }
    }

    /**
     * Launching the billing flow.
     *
     * Launching the UI to make a purchase requires a reference to the Activity.
     */
    private fun internalLaunchBillingFlow(
        activity: Activity?,
        @BillingClient.SkuType skuType: String,
        productId: String
    ): BillingResult {
        log("$TAG launchBillingFlow: skuType: $skuType productId $productId")
        if (activity == null) {
            logE("launchBillingFlow: skuType: $skuType productId $productId Activity is null") // UI已释放 // USER_CANCELED
            return provideBillingResult(BillingClient.BillingResponseCode.ERROR, "Activity is null")
        }
        if (billingClient?.isReady != true) {
            logE("launchBillingFlow: skuType: $skuType productId $productId BillingClient is not ready")
            return provideBillingResult(BillingClient.BillingResponseCode.ERROR, "BillingClient is not ready")
        }
        val skuDetails: SkuDetails? = when (skuType) {
            BillingClient.SkuType.INAPP -> inappSkuDetails.value?.get(productId)
            BillingClient.SkuType.SUBS -> subsSkuDetails.value?.get(productId)
            else -> null
        }
        if (skuDetails == null) {
            logE("launchBillingFlow: skuType: $skuType productId $productId SkuDetails is null") // BILLING_UNAVAILABLE // product详情拿不到or尚未拿到
            return provideBillingResult(BillingClient.BillingResponseCode.ERROR, "SkuDetails is null")
        }
        log("$TAG launchBillingFlow: skuType: $skuType productId $productId details -> $skuDetails")
        val params = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).build()
        val result: BillingResult? = billingClient?.launchBillingFlow(activity, params)
        if (result == null) {
            logE("launchBillingFlow: skuType: $skuType productId $productId BillingClient is disconnected") // SERVICE_DISCONNECTED // billingClient已释放
            return provideBillingResult(BillingClient.BillingResponseCode.ERROR, "BillingClient is disconnected")
        }
        return result
    }

    private fun provideBillingClient(): BillingClient {
        return BillingClient.newBuilder(appContext)
            .setListener(this)
            .enablePendingPurchases() // Not used for subscriptions.
            .build()
    }

    private fun sentPurchaseUpdateEvent(
        billingResult: BillingResult,
        purchasesList: List<Purchase>
    ) {
        // buy success event start
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            Handler(Looper.getMainLooper()).post {
                purchaseUpdateEvent.value = purchasesList
                purchaseUpdateEvent.value = null
            }
        }
        // buy success event end
    }

    private fun sentPurchaseSyncedMissedEvent() {
        log("$TAG sentPurchaseSyncedMissedEvent appBillingClient")
        Handler(Looper.getMainLooper()).post {
            purchaseSyncedMissedEvent.value = Any()
            purchaseSyncedMissedEvent.value = null
        }
    }

    // setup google pay services
    fun setup(lifecycle: Lifecycle, refer: String?, silently: Boolean) {
        require(isMainThread())
        lifecycleRef?.removeObserver(this)
        this.lifecycleRef = lifecycle
        this.silently = silently
        this.refer = refer
        lifecycle.addObserver(this)
        log("$TAG setup lifecycle")
    }

    private fun feedback(step: String, remark: String) {
        feedback(TAG, step, remark, retry = 1)
        analysisEvent(step)
    }

    private fun billingToast(msg: String?) {
        if (!silently) toast(msg)
    }

    private fun respCodeDesc(@BillingClient.BillingResponseCode value: Int): String {
        return when (value) {
            BillingClient.BillingResponseCode.OK -> "OK"
            BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> "SERVICE_DISCONNECTED"
            BillingClient.BillingResponseCode.SERVICE_TIMEOUT -> "SERVICE_TIMEOUT"
            BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> "SERVICE_UNAVAILABLE"
            BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> "BILLING_UNAVAILABLE"
            BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> "ITEM_UNAVAILABLE"
            BillingClient.BillingResponseCode.DEVELOPER_ERROR -> "DEVELOPER_ERROR"
            BillingClient.BillingResponseCode.ERROR -> "ERROR"
            BillingClient.BillingResponseCode.USER_CANCELED -> "USER_CANCELED"
            BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> "FEATURE_NOT_SUPPORTED"
            BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> "ITEM_ALREADY_OWNED"
            BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> "ITEM_NOT_OWNED"
            else -> "UNKNOWN"
        }
    }

    @Suppress("SameParameterValue")
    private fun provideBillingResult(respCode: Int, msg: String = ""): BillingResult {
        return BillingResult.newBuilder()
            .setResponseCode(respCode).setDebugMessage(msg).build()
    }
}